package com.reder.practice.common.util;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Base64;

/**
 * 图片工具类
 * 用法：
 * <ol>
 * <li>ValidatedCodeUtil.generateImage("Z:/", "code")</li>
 * <li>ValidatedCodeUtil.generateImageAsBase64()</li>
 * </ol>
 * <p></p>
 * 推荐使用第二种，因为返回 base64 编码可以直接内嵌进 html，
 * 不用再发送一次请求来获取图片，而且第二种不用操作硬盘 IO，
 * 可以提高生成的效率，降低生成的出错率
 * <p></p>
 * 上面两种都会返回一个 CodeImage 对象，里面包含了原始验证码（用于后台验证）
 * 和验证码图片数据（用于前台展示），分别通过 getCode() 和 getImgSrc() 可以获得
 * <p></p>
 *
 * @author Fish
 * 1149062639@qq.com
 * created by 2018/08/05 14:22:42
 */
public final class ValidatedCodeUtil {

    // 验证码内部类，外部可以得到这个类的对象，从而取得需要的数据
    public static final class CodeImage {

        private final String code; // 原始验证码信息
        private final String imgSrc; // 图片引用地址

        CodeImage(String code, String imgSrc) {
            this.code = code;
            this.imgSrc = imgSrc;
        }

        public String getCode() {
            return code;
        }

        public String getImgSrc() {
            return imgSrc;
        }

        @Override
        public String toString() {
            return "CodeImage{" +
                    "code='" + code + '\'' +
                    ", imgSrc='" + imgSrc + '\'' +
                    '}';
        }
    }

    // 干扰线的条数
    private static final int LINE_COUNT = 40;

    // 默认验证码的长度
    private static int codeLength = 4;

    // 一个字符占用宽度
    private static final int ONE_CHAR_WIDTH = 50;

    // 图片验证码的宽度
    private static int width = ONE_CHAR_WIDTH * codeLength;

    // 图片验证码的高度
    private static int height = 80;

    // 验证码中可以出现的字符
    private static final char[] CHARS = "abcdefghijklmnopqrstuvwxyz1234567890".toCharArray();

    // 字体范围值，第一个为最小值，第二个为最大值
    private static final int[] FONT_SIZES = {25, 75};

    // 默认保存格式
    private static final String DEFAULT_FORMAT = "png";

    // base64 图片数据头
    private static final String BASE64_IMAGE_HEADER = "data:image/" + DEFAULT_FORMAT + ";base64,";

    /**
     * 设置验证码图片长度
     *
     * @param length 要设置成的长度
     */
    public static void setCodeLength(int length) {
        codeLength = length;
        width = ONE_CHAR_WIDTH * codeLength;
    }

    // 返回 [min, max) 之间的一个随机数
    private static int getRandomIndex(int min, int max) {
        return (int) (Math.random() * (max - min) + min);
    }

    // 随机生成 codeLength 个字符
    private static char[] getRandomChars() {
        // 返回这个生成好的字符数组，包含了要被绘制的验证码字符
        char[] codes = new char[codeLength];

        int length = CHARS.length;
        for (int i = 0; i < codeLength; i++) {
            codes[i] = CHARS[getRandomIndex(0, length)];
        }

        return codes;
    }

    // 随机生成颜色值，返回 Color 对象
    private static Color getRandomColor() {
        return new Color(getRandomIndex(0, 256), getRandomIndex(0, 256), getRandomIndex(0, 256));
    }

    // 随机生成字体，返回 Font 对象
    private static Font getRandomFont(int fontSize) {
        return new Font(Font.SANS_SERIF, Font.BOLD, fontSize);
    }

    // 将字符数组画到图片上
    private static void drawCodes(Graphics g, char[] codes) {
        for (int i = 0; i < codes.length; i++) {

            // 随机生成这个字符的字体和颜色
            int fontSize = getRandomIndex(FONT_SIZES[0], FONT_SIZES[1]);
            g.setFont(getRandomFont(fontSize));
            g.setColor(getRandomColor());

            // 将字符画上去
            g.drawChars(codes, i, 1, i * ONE_CHAR_WIDTH, fontSize);
        }
    }

    // 将很多组干扰线画到图片上
    private static void makeSomeNoise(Graphics g) {
        for (int i = 0; i < LINE_COUNT; i++) {

            // 每一条线都是随机颜色
            g.setColor(getRandomColor());
            int x1 = getRandomIndex(0, width);
            int y1 = getRandomIndex(0, height);
            int x2 = getRandomIndex(0, width);
            int y2 = getRandomIndex(0, height);

            g.drawLine(x1, y1, x2, y2);
        }
    }

    // 根据传入的 codes 生成一幅验证码图片
    private static BufferedImage getImageFromChars(char[] codes) {

        BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = bi.getGraphics();

        // 填充背景为白色
        g.setColor(new Color(255, 255, 255));
        g.fillRect(0, 0, width, height);

        // 画验证码
        drawCodes(g, codes);

        // 设置干扰
        makeSomeNoise(g);

        // 释放画笔
        g.dispose();

        return bi;
    }

    /**
     * 使用默认格式 png 保存图片到本地
     * <p></p>
     *
     * @param path 保存到什么地方，要包含文件名，但是后缀名会自动加上
     */
    public static CodeImage generateImage(String path, String fileName) {

        // 随机生成一组字符验证码
        char[] codes = getRandomChars();

        File file = new File(path + File.separator + fileName + "." + DEFAULT_FORMAT);
        try {
            // 使用默认格式 png 保存图片到本地
            ImageIO.write(getImageFromChars(codes), DEFAULT_FORMAT, file);
        } catch (IOException e) {
            System.out.println("图片生成失败...");
            return null;
        }

        return new CodeImage(new String(codes), file.toString());
    }

    /**
     * 生成验证码，以 base64 编码返回
     *
     * @return 返回验证码的 base64 形式
     */
    public static CodeImage generateImageAsBase64() {
        // 随机生成一组字符验证码
        char[] codes = getRandomChars();

        // 生成缓存图片
        BufferedImage bi = getImageFromChars(codes);

        // 写入内存中的输出流
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {

            // 将图片写入输出流中，为了得到 byte[]
            ImageIO.write(bi, DEFAULT_FORMAT, out);
        } catch (IOException e) {
            System.out.println("图片生成失败...");
            e.printStackTrace();
        }

        // 返回 html 中 img 标签能用的图片数据
        String imageBase64 = new String(Base64.getEncoder().encode(out.toByteArray()));
        String imgSrc = BASE64_IMAGE_HEADER + imageBase64;

        return new CodeImage(new String(codes), imgSrc);
    }
}
